/**
 * Licensed to Jasig under one or more contributor license
 * agreements. See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership.
 * Jasig licenses this file to you under the Apache License,
 * Version 2.0 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a
 * copy of the License at:
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.jasig.portal.portlet.container.services;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.Lock;

import javax.portlet.ValidatorException;
import javax.servlet.http.HttpServletRequest;

import org.apache.pluto.container.PortletRequestContext;
import org.apache.pluto.container.om.portlet.PortletDefinition;
import org.apache.pluto.container.om.portlet.Preference;
import org.apache.pluto.container.om.portlet.Preferences;
import org.jasig.portal.portlet.dao.jpa.PortletPreferenceImpl;
import org.jasig.portal.portlet.om.IPortletDefinition;
import org.jasig.portal.portlet.om.IPortletDefinitionId;
import org.jasig.portal.portlet.om.IPortletEntity;
import org.jasig.portal.portlet.om.IPortletEntityId;
import org.jasig.portal.portlet.om.IPortletPreference;
import org.jasig.portal.portlet.registry.IPortletDefinitionRegistry;
import org.jasig.portal.portlet.registry.IPortletEntityRegistry;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionOperations;

/**
 * Preferences impl that manipulates the portlet entity level preference data
 * 
 * @author Eric Dalquist
 */
public class PortletEntityPreferencesImpl extends AbstractPortletPreferencesImpl<IPortletEntity> {
    private final PortletRequestContext portletRequestContext;
    private final IPortletEntityId portletEntityId;
    
    private final IPortletEntityRegistry portletEntityRegistry;
    private final IPortletDefinitionRegistry portletDefinitionRegistry;
    private final TransactionOperations transactionOperations;

    public PortletEntityPreferencesImpl(PortletRequestContext portletRequestContext,
            IPortletEntityRegistry portletEntityRegistry, IPortletDefinitionRegistry portletDefinitionRegistry,
            TransactionOperations transactionOperations, IPortletEntityId portletEntityId, boolean render) {
        super(render);
        
        this.portletRequestContext = portletRequestContext;
        this.portletEntityRegistry = portletEntityRegistry;
        this.portletDefinitionRegistry = portletDefinitionRegistry;
        this.transactionOperations = transactionOperations;
        this.portletEntityId = portletEntityId;
    }
    

    @Override
    protected IPortletEntity getInitializationContext() {
        final HttpServletRequest containerRequest = this.portletRequestContext.getContainerRequest();
        return this.portletEntityRegistry.getPortletEntity(containerRequest, portletEntityId);
    }

    @Override
    protected Object getLogDescription() {
        return this.getInitializationContext();
    }

    @Override
    protected void loadTargetPortletPreferences(IPortletEntity portletEntity, Map<String, IPortletPreference> targetPortletPreferences) {
        final List<IPortletPreference> entityPreferences = portletEntity.getPortletPreferences();
        for (final IPortletPreference preference : entityPreferences) {
            targetPortletPreferences.put(preference.getName(), preference);
        }        
    }

    @Override
    protected void loadBasePortletPreferences(IPortletEntity portletEntity, Map<String, IPortletPreference> basePortletPreferences) {
        final IPortletDefinition portletDefinition = portletEntity.getPortletDefinition();
        
        //Add descriptor prefs to base Map
        final IPortletDefinitionId portletDefinitionId = portletDefinition.getPortletDefinitionId();
        final PortletDefinition portletDescriptor = this.portletDefinitionRegistry.getParentPortletDescriptor(portletDefinitionId);
        final Preferences descriptorPreferences = portletDescriptor.getPortletPreferences();
        for (final Preference preference : descriptorPreferences.getPortletPreferences()) {
            final IPortletPreference preferenceWrapper = new PortletPreferenceImpl(preference);
            basePortletPreferences.put(preferenceWrapper.getName(), preferenceWrapper);
        }

        //Add definition prefs to base Map
        final List<IPortletPreference> definitionPreferences = portletDefinition.getPortletPreferences();
        for (final IPortletPreference preference : definitionPreferences) {
            basePortletPreferences.put(preference.getName(), preference);
        }
    }

    @Override
    protected boolean storeInternal() throws IOException, ValidatorException {
        final HttpServletRequest containerRequest = portletRequestContext.getContainerRequest();
        
        final IPortletEntity portletEntity = this.portletEntityRegistry.getPortletEntity(containerRequest, portletEntityId);
        final IPortletEntityId portletEntityId = portletEntity.getPortletEntityId();
        final Lock portletEntityLock = this.portletEntityRegistry.getPortletEntityLock(containerRequest, portletEntityId);
        
        //Do a tryLock first so that we can warn about concurrent preference modification if it fails
        boolean locked = portletEntityLock.tryLock();
        try {
            if (!locked) {
                logger.warn("Concurrent portlet preferences modification by: " + portletEntity +  " " +
                        "This has the potential for changes to preferences to be lost. " +
                        "This portlet should be modified to synchronize its preference modifications appropriately", new Throwable());
                
                portletEntityLock.lock();
                locked = true;
            }
            
            return this.transactionOperations.execute(new TransactionCallback<Boolean>() {
                @Override
                public Boolean doInTransaction(TransactionStatus status) {
                    //Refresh the entity to avoid optimistic locking errors
                    final IPortletEntity portletEntity = portletEntityRegistry.getPortletEntity(containerRequest, portletEntityId);
                
                    final Map<String, IPortletPreference> targetPortletPreferences = getTargetPortletPreferences();
                    final Collection<IPortletPreference> values = targetPortletPreferences.values();
                    final boolean modified = portletEntity.setPortletPreferences(new ArrayList<IPortletPreference>(values));
                    if (!modified) {
                        //Nothing actually changed, skip the store
                        return Boolean.FALSE;
                    }
                    
                    portletEntityRegistry.storePortletEntity(containerRequest, portletEntity);
                    
                    return Boolean.TRUE;
                }
            });
        }
        finally {
            //check if locked, needed due to slightly more complex logic around the tryLock and logging
            if (locked) {
                portletEntityLock.unlock();
            }
        }
    }
}
